/**
 * LockboxStorage
 * Manages storage and encryption of users secret information
 *
 * Created by Nathan Black
 */

#import "LockboxStorage.h"
#import "LockboxItem.h"
#import "LockboxOptions.h"
#import "parseCSV.h"

NSString* const NSFileIOException = @"NSFileIOException";
NSString* const LockboxInvalidKeyException = @"LockboxInvalidKeyException";
NSString* const LockboxEncryptionException = @"LockboxEncryptionException";

static LockboxStorage *s_lbStorageInstance;

@interface LockboxStorage (Private)

- (void) refreshTagCache;

@end

@implementation LockboxStorage

+ (LockboxStorage *)globalInstance
{
    if (s_lbStorageInstance == NULL) {
        s_lbStorageInstance = [[LockboxStorage alloc] init];
    }
    return s_lbStorageInstance;
}

-init 
{
    if ((self = [super init])) {
        _items = [[NSMutableArray alloc] init];
        _tagCache = [[NSMutableArray alloc] init];
        
        _filepath = [[[LockboxOptions homedir] stringByAppendingString:@"/Lockbox_v1.dat"] retain];
        _algorithm = @"aes128";
    }
    return self;
}

- (void)dealloc
{
    [_key release];
    _key = NULL;

    [_items release];
    _items = NULL;

    [_tagCache release];
    _tagCache = NULL;
    
    [_filepath release];
    _filepath = NULL;
    
    [super dealloc];
}

- (BOOL)exists
{
    return [[NSFileManager defaultManager] fileExistsAtPath:_filepath];
}

- (BOOL)loadWithKey:(NSString*)key
{
    NSLog(@"load enter");
    [_key release];
    _key = [key dataUsingEncoding:NSUTF8StringEncoding];
    [_key retain];

    if ([[NSFileManager defaultManager] fileExistsAtPath:_filepath]) {
        NSFileHandle* fileH;
        NSData* cipher;
        SSCrypto* crypto;
        NSLog(@"file does exist");
        @try {
            fileH = [NSFileHandle fileHandleForReadingAtPath:_filepath];
            if (fileH == NULL) [NSException raise:NSFileIOException format:@"Unable to open file"];
        
            cipher = [fileH readDataToEndOfFile];
            if (cipher == NULL || [cipher length] == 0) [NSException raise:NSFileIOException format:@"readDataToEndOfFile returned null or empty data"];
            
            crypto = [[SSCrypto alloc] initWithSymmetricKey:_key];
            [crypto setCipherText:cipher];
            NSData* plain = [crypto decrypt:_algorithm];
            if (plain == NULL) [NSException raise:LockboxInvalidKeyException format:@"Error with decryption using %@", _algorithm];
            
            NSMutableArray *temp = [NSKeyedUnarchiver unarchiveObjectWithData:plain];
            if (temp == NULL) [NSException raise:NSFileIOException format:@"unarchived object was null"];

            if ([temp count] > 0) {
                [_items release];
                _items = temp;
                [_items retain];
            }
            
            [self refreshTagCache];
            NSLog(@"Data read successfully");
        }
        @catch (NSException* ex) {
            NSLog(@"load catch");
            @throw;
        }
        @finally {
            [crypto release];
            [fileH closeFile];
            //[fileH release];
            //[cipher release];
        }
    }
    else {
        //[self loadDummyData];
    }
    
    [self validateData];
    NSLog(@"load exit");
}

/*
 * Load CSV file with following format:
 * Type;Key;Value;Additional Info;Website;Exp. Date;Phone;Notes;Tags
 *
 * Type: can be one of "Password","Card", or "Other"
 * Key, Value, Additional Info, Website, Exp. Date, Phone: just plain text
 * Notes: Should be enclosed by quotation marks and can contain multiple lines
 * Tags: Should be enclused by quotation marks and separated by commas
 *
 * Please note:
 * - for Password type just index 0,1,2,4,7,8 are used
 * - for Card type just index 0,1,2,3,5,6,7,8 are used
 * - for Other type just index 0,1,2,7,8 are used
 */
- (BOOL)loadCSV:(NSString*)filepath;
{
  NSLog(@"load CSV:%@", filepath);

  if ([[NSFileManager defaultManager] fileExistsAtPath:filepath]) {
    NSLog(@"file does exist");
    // Creating CSV parser and parsing file
    CSVParser *parser = [CSVParser new];
    @try {
      if (![parser openFile:filepath])
        [NSException raise:NSFileIOException format:@"Unable to open CSV file"];

      [parser setDelimiter: ';'];
      NSArray *csvContent = [parser parseFile];
  	  NSLog(@"CSV file has %d non-empty lines.", [csvContent count]);

      // adding entries to item list
      int line;
      for (line = 0; line < [csvContent count]; line++) {
        NSMutableArray *csvLine = [csvContent objectAtIndex: line];
        if([csvLine count] == 9) {
          NSString* type = [csvLine objectAtIndex:0];
          if([type caseInsensitiveCompare:@"password"] == NSOrderedSame) {
            LockboxItem *item = [[[LockboxItem alloc] initWithType:(int)LBI_PASSWORD] autorelease];            item->key = [[csvLine objectAtIndex:1] retain];
            item->value = [[csvLine objectAtIndex:2] retain];
            item->website = [[csvLine objectAtIndex:4] retain];
            item->note = [[csvLine objectAtIndex:7] retain];
            NSString *tagstring = [csvLine objectAtIndex:8];
            if([tagstring length]>0)
              item->tags = [[tagstring componentsSeparatedByString:@","] mutableCopyWithZone:NULL];
        	  //NSLog(@"CSV: adding password\n  key='%@'\n  value='%@'\n  website='%@'\n  note=<%@>\n  tags=%@", 
        	  //  item->key, item->value, item->website, item->note, item->tags);
            [_items addObject:item];
          } else if([type caseInsensitiveCompare:@"card"] == NSOrderedSame) {
            LockboxItem *item = [[[LockboxItem alloc] initWithType:(int)LBI_CARD] autorelease];
            item->key = [[csvLine objectAtIndex:1] retain];
            item->value = [[csvLine objectAtIndex:2] retain];
            item->additionalInfo = [[csvLine objectAtIndex:3] retain];
            item->expDate = [[csvLine objectAtIndex:5] retain];
            item->phone1 = [[csvLine objectAtIndex:6] retain];
            item->note = [[csvLine objectAtIndex:7] retain];
            NSString *tagstring = [csvLine objectAtIndex:8];
            if([tagstring length]>0)
              item->tags = [[tagstring componentsSeparatedByString:@","] mutableCopyWithZone:NULL];
        	  //NSLog(@"CSV: adding card\n  key='%@'\n  value='%@'\n  additionalInfo='%@'\n  expDate='%@'\n  phone1='%@'\n  note=<%@>\n  tags=%@", 
        	  //  item->key, item->value, item->additionalInfo, item->expDate, item->phone1, item->note, item->tags);
            [_items addObject:item];
          } else if([type caseInsensitiveCompare:@"other"] == NSOrderedSame) {
            LockboxItem *item = [[[LockboxItem alloc] initWithType:(int)LBI_OTHER] autorelease];
            item->key = [[csvLine objectAtIndex:1] retain];
            item->value = [[csvLine objectAtIndex:2] retain];
            item->note = [[csvLine objectAtIndex:7] retain];
            NSString *tagstring = [csvLine objectAtIndex:8];
            if([tagstring length]>0)
              item->tags = [[tagstring componentsSeparatedByString:@","] mutableCopyWithZone:NULL];
        	  //NSLog(@"CSV: adding other\n  key='%@'\n  value='%@'\n  note=<%@>\n  tags=%@", 
        	  //  item->key, item->value, item->note, item->tags);
            [_items addObject:item];
          } else
            NSLog(@"line %d has illegal type: %@", line, csvLine);
        } else
          NSLog(@"line %d ignored: %@", line, csvLine);
      } // for
    }
    @catch (NSException* ex) {
      NSLog(@"load CSV catch");
      @throw;
    }
    @finally {
      [parser closeFile];
      [parser release];
    }
  }

  [self refreshTagCache];
  [self validateData];
  NSLog(@"load CSV exit");
}

- (void) loadDummyData
{
    LockboxItem *item = [[[LockboxItem alloc] initWithType:(int)LBI_PASSWORD] autorelease];
    item->key = @"Phylar";
    item->value = @"notmypassword";
    item->website = @"www.hackint0sh.org";
    [_items addObject:item];

    item = [[[LockboxItem alloc] initWithType:(int)LBI_OTHER] autorelease];
    item->key = @"SSN";
    item->value = @"123-45-6789";
    [_items addObject:item];

    item = [[[LockboxItem alloc] initWithType:(int)LBI_CARD] autorelease];
    item->key = @"DiscoverCard";
    item->value = @"1122 3344 5566 7788";
    item->expDate = @"01/10";
    item->additionalInfo = @"123";
    item->phone1 = @"18003472683";
    [_items addObject:item];
}


- (NSMutableArray *)items
{
    return _items;
}

- (NSMutableArray*)allTags
{
    return _tagCache;
}

// Remove a tag from all items
- (void)purgeTag:(NSString*)tagToDelete
{   
    NSLog(@"purgeTag: %@", tagToDelete);
    NSEnumerator* itemEnum = [_items objectEnumerator];
    LockboxItem* item;
    while ((item = [itemEnum nextObject])) {
        int i;
        for (i = 0; i < [item->tags count]; i++) {
            if ([tagToDelete caseInsensitiveCompare:[item->tags objectAtIndex:i]] == NSOrderedSame) {
                [item->tags removeObjectAtIndex:i];
                break;
            }
        }
    }
}

- (void)renameTag:(NSString*)oldTag newTag:(NSString*)newTag
{   
    NSLog(@"renameTag: %@ to %@", oldTag, newTag);
    NSEnumerator* itemEnum = [_items objectEnumerator];
    LockboxItem* item;
    while ((item = [itemEnum nextObject])) {
        int i;
        for (i = 0; i < [item->tags count]; i++) {
            if ([oldTag caseInsensitiveCompare:[item->tags objectAtIndex:i]] == NSOrderedSame) {
                [item->tags removeObjectAtIndex:i];
                [item->tags addObject:newTag];
                break;
            }
        }
    }
}

- (void)refreshTagCache
{
    [_tagCache removeAllObjects];
    
    LockboxItem* item;
    NSString* tag;
    NSString* itemTag;
    // iterate through all the item's tags
    NSEnumerator* itemEnum = [_items objectEnumerator];
    while ((item = [itemEnum nextObject])) {
        NSEnumerator* tagEnum = [item->tags objectEnumerator];
        while ((tag = [tagEnum nextObject])) {
            // check to see if we've added this tag to the cache already. If not, add it.
            BOOL bFound = false;
            NSEnumerator* itemTagEnum = [_tagCache objectEnumerator];
            while ((itemTag = [itemTagEnum nextObject])) {
                if ([itemTag compare:tag] == NSOrderedSame) {
                    bFound = true;
                break;
                }
            }
            
            if (!bFound) {
                [_tagCache addObject:tag];
            }
        }
    }
    
}

- (void)changeKey:(NSString*)newKey
{
    [_key release];
    _key = [newKey dataUsingEncoding:NSUTF8StringEncoding];
    [_key retain];
    [self save];
}

- (void)save
{
    // TODO: dirty flags would be nice to reduce writing the file all the time
    SSCrypto* crypto;
    @try {
        if (_key) {
            NSData* plain = [NSKeyedArchiver archivedDataWithRootObject:_items];
            
            crypto = [[SSCrypto alloc] initWithSymmetricKey:_key];
            [crypto setClearTextWithData:plain];
            NSData* cipher = [crypto encrypt:_algorithm];
            if (cipher == NULL) [NSException raise:LockboxEncryptionException format:@"Error with encryption using %@", _algorithm];

            if ([[NSFileManager defaultManager] fileExistsAtPath:_filepath]) {
                if ([[NSFileManager defaultManager] fileExistsAtPath:_filepath] == NO) {
                    [NSException raise:NSFileIOException format:@"Unable to delete old file before writing"];
                }
            }

            BOOL result = [[NSFileManager defaultManager] createFileAtPath:_filepath contents:cipher attributes:NULL];
            if (result == NO) [NSException raise:NSFileIOException format:@"Unable to create and write file"];
            NSLog(@"write succedded");
        }
    }
    @finally {
        NSLog(@"save finally block");
        [crypto release];
    }
}

- (void)validateData
{
    NSAssert(_items != NULL, @"items container must never be null");
    int i;
    for (i = 0; i < [_items count]; i++) {
        LockboxItem* item = [_items objectAtIndex:i];
        NSAssert1(item->type >= 0 && item->type < LBI_NUMTYPES, @"item type is out of range: %d", item->type);
        
        NSAssert(item->key != NULL, @"item key is null");
        NSAssert(item->value != NULL, @"item value is null");
        switch (item->type) {
            case LBI_PASSWORD:
                NSAssert(item->website != NULL, @"item website for password type is null");
                break;
            case LBI_CARD:
                NSAssert(item->expDate != NULL, @"item expDate for card type is null");
                break;
        }
    }
}

@end
